﻿//	Copyright (c) 2012 Jonathan Loe
//
//	MIT license (http://en.wikipedia.org/wiki/MIT_License)
//
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights 
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
//	of the Software, and to permit persons to whom the Software is furnished to do so, 
//	subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in all 
//	copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//	PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
//	FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
//	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Text;
using System.Transactions;

using Ordinaire.Db;

namespace Ordinaire
{
    /// <summary>
    /// Represents a base model of data manager which provides common database-related functions.
    /// </summary>
    /// <typeparam name="TContext">type of DataContext to use</typeparam>
    /// <remarks>This is provided to give the convenience for all derived classes to use the assigned data context.</remarks>
    public abstract class DataManager<TContext>
        where TContext : OContext
    {
        #region Variables

        /// <summary>
        /// The default userid used as the creator's userid when inserting data into database.
        /// </summary>
        public const string DefaultUserId = "ordinaire";

        /// <summary>
        /// The current user name that instantiates this.
        /// </summary>
        private string currentUserId;

        /// <summary>
        /// Storage for instantiated or desired context.
        /// </summary>
        /// <remarks>Useful especially when transaction is being used</remarks>
        protected TContext context;

        #endregion

        #region Property methods

        /// <summary>
        /// Returns new data context of default type if none specified, else the new data context of desired type.
        /// </summary>
        public virtual TContext Context
        {
            get
            {
                if (context == null)
                {
                    context = DbFactory.Instance.CreateDataContext<TContext>();

                    // get DataLoadOptions, if specified, and assign it to the DataContext.
                    DataLoadOptions loadOptions = GetDataLoadOptions();
                    if (loadOptions != null)
                    {
                        context.LoadOptions = loadOptions;
                    }
                }
                return context;
            }
            set { context = value; }
        }

        /// <summary>
        /// Gets the current user id that instantiates this.
        /// </summary>
        public string CurrentUserId
        {
            get
            {
                return (String.IsNullOrEmpty(currentUserId)) ? DefaultUserId : this.currentUserId;
            }
            set
            {
                this.currentUserId = value;
            }
        }

        #endregion

        #region Public methods

        #region virtual

        /// <summary>
        /// Returns specific DataLoadOptions for derived DataManager classes.
        /// </summary>
        /// <returns>the DataLoadOptions</returns>
        /// <remarks>overrides this function to enable eager loading to repository entities</remarks>
        public virtual DataLoadOptions GetDataLoadOptions()
        {
            return null;
        }

        #endregion

        #region Static

        /// <summary>
        /// Returns new TransactionScope instance for no-lock transaction.
        /// </summary>
        /// <returns>new no-lock TransactionScope</returns>
        public static TransactionScope CreateNoLockTransaction()
        {
            return new TransactionScope(TransactionScopeOption.Required, new TransactionOptions {
                IsolationLevel = IsolationLevel.ReadUncommitted
            });
        }

        #endregion

        #region Count

        /// <summary>
        /// Returns the number of data found with the specified id.
        /// </summary>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <param name="id">the id</param>
        /// <returns>the number of data found with the specified id</returns>
        public virtual int Count<TTable, TId, TWho>(TId id)
            where TTable : Data<TId, TWho>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.Id.Equals(id))
                .Count();
        }

        /// <summary>
        /// Returns the number of data found with the specified name.
        /// </summary>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <param name="name">name</param>
        /// <param name="ignoreCase">true, as default, to ignore case during the comparison; otherwise, false</param>
        /// <returns>the number of data found with the specified name</returns>
        public virtual int CountByName<TTable, TId, TWho>(string name, bool ignoreCase = true)
            where TTable : Data<TId, TWho>
        {
            if (ignoreCase)
            {
                name = name.ToLower();

                return Context
                    .GetTable<TTable>()
                    .Where(d => d.Name.ToLower().Equals(name))
                    .Count();
            }
            else
            {
                return Context
                    .GetTable<TTable>()
                    .Where(d => d.Name.Equals(name))
                    .Count();
            }
        }

        #endregion

        #region Exists

        /// <summary>
        /// Returns a value indicating where the specified id exists.
        /// </summary>
        /// <param name="id">id</param>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <returns>true if exists, else false</returns>
        public virtual bool Exists<TTable, TId, TWho>(TId id)
            where TTable : Data<TId, TWho>
        {
            id.ThrowIfNull<ArgumentNullException>("id");
            return (Count<TTable, TId, TWho>(id) > 0) ? true : false;
        }

        /// <summary>
        /// Returns a value indicating where the specified name exists.
        /// </summary>
        /// <param name="name">name</param>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <returns>true if exists, else false</returns>
        public virtual bool ExistsByName<TTable, TId, TWho>(string name)
            where TTable : Data<TId, TWho>
        {
            name.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "name"));
            return (CountByName<TTable, TId, TWho>(name) > 0) ? true : false;
        }

        #endregion

        #region Retrieval

        /// <summary>
        /// Returns data with the specified id
        /// </summary>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <param name="id">the id</param>
        /// <returns>data with the specified id</returns>
        public virtual TTable Get<TTable, TId, TWho>(TId id)
            where TTable : Data<TId, TWho>
        {
            // Equals() is used due to known LINQ bug as suggested http://stackoverflow.com/questions/3027098/vs-2010-entity-repository-error
            // see: http://connect.microsoft.com/VisualStudio/feedback/details/394255/linq-to-sql-bug-handling-entities-with-common-base-class
            return Context
                .GetTable<TTable>()
                .Where(d => d.Id.Equals(id))
                .SingleOrDefault();
        }

        /// <summary>
        /// Returns data with the specified name.
        /// </summary>
        /// <param name="name">name</param>
        /// <param name="ignoreCase">true, as default, to ignore case during the comparison; otherwise, false</param>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <returns>a match from the table or throws exception if returns more than one elements</returns>
        public virtual TTable GetByName<TTable, TId, TWho>(string name, bool ignoreCase = true)
            where TTable : Data<TId, TWho>
        {
            name.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "name"));

            var query = from list in Context.GetTable<TTable>()
                        select list;

            if (ignoreCase)
            {
                query = from list in query
                        where list.Name.ToLower().Equals(name.ToLower())
                        select list;
            }
            else
            {
                query = from list in query
                        where list.Name.Equals(name)
                        select list;
            }

            return query.SingleOrDefault();
        }

        /// <summary>
        /// Returns data with the specified name.
        /// </summary>
        /// <param name="names">array of names</param>
        /// <param name="ignoreCase">true, as default, to ignore case during the comparison; otherwise, false</param>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <returns>list of matches from the table</returns>
        public virtual List<TTable> GetByNames<TTable, TId, TWho>(string[] names, bool ignoreCase = true)
            where TTable : Data<TId, TWho>
        {
            names.ThrowIfContainsNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.ContainsNullOrEmpty, "names"));

            var query = from list in Context.GetTable<TTable>()
                        select list;

            if (ignoreCase)
            {
                query = from list in query
                        where names.ToLower().Contains(list.Name.ToLower())
                        select list;
            }
            else
            {
                query = from list in query
                        where names.Contains(list.Name)
                        select list;
            }

            return query.ToList();
        }

        /// <summary>
        /// Returns the data id with the specified name.
        /// </summary>
        /// <param name="name">name</param>
        /// <param name="ignoreCase">true, as default, to ignore case during the comparison; otherwise, false</param>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <returns>the id or throws exception if multiple data found</returns>
        public virtual TId GetIdByName<TTable, TId, TWho>(string name, bool ignoreCase = true)
            where TTable : Data<TId, TWho>
        {
            name.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "name"));

            var query = from list in Context.GetTable<TTable>()
                        select list;

            if (ignoreCase)
            {
                query = from list in query
                        where list.Name.ToLower().Equals(name.ToLower())
                        select list;
            }
            else
            {
                query = from list in query
                        where list.Name.Equals(name)
                        select list;
            }

            return query.Select(d => d.Id).SingleOrDefault();
        }

        #endregion

        #region Insertion

        /// <summary>
        /// Inserts list of new data into the specified TTable table.
        /// </summary>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <param name="data">data to be inserted</param>
        /// <returns>new data with updated values</returns>
        public virtual TTable[] Insert<TTable, TId, TWho>(params TTable[] data)
            where TTable : Data<TId, TWho>
        {
            data.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "data"));

            // Set column values
            foreach (TTable record in data)
            {
                record.CreatedBy = (TWho)(object)CurrentUserId;
            }

            OContext context = Context;
            context.GetTable(typeof(TTable)).InsertAllOnSubmit(data);
            context.SubmitChanges();

            return data;
        }

        /// <summary>
        /// Clear data context cache.
        /// </summary>
        public void ClearContextCache()
        {
            // Prevent DataContext from caching current data, instead to refresh its entity when requested.
            // This also allows EntitySet eager loading to work, if used.
            // refer to:
            // - http://stackoverflow.com/questions/2141208/linq-to-sql-datacontext-caching
            // - http://stackoverflow.com/questions/2098143/how-to-clear-the-datacontext-cache-on-linq-to-sql
            context.ClearCache();
        }

        #endregion

        #region Update

        /// <summary>
        /// Updates the array of specified data into the data source.
        /// </summary>
        /// <typeparam name="TTable">type of mapping table</typeparam>
        /// <typeparam name="TId">type of data unique identifier</typeparam>
        /// <typeparam name="TWho">type of data IAudit</typeparam>
        /// <param name="data">data to be updated</param>
        /// <returns>new data with updated values</returns>
        public virtual TTable[] Update<TTable, TId, TWho>(params TTable[] data)
            where TTable : Data<TId, TWho>
        {
            data.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "data"));

            // Set column values
            foreach (TTable record in data)
            {
                record.UpdatedBy  = (TWho)(object)CurrentUserId;
                record.UpdateDate = DateTime.Now;
            }

            Context.SubmitChanges();

            return data;
        }

        #endregion

        #endregion
    }
}
